﻿using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Text;

namespace NW
{
	public delegate void NWClientEvent(NWClient sender);
	public delegate void NWClientDataEvent(NWClient sender, byte[] data);
	public delegate void NWClientStringEvent(NWClient sender, string data);
	public class NWClient
	{
		public string ClientHeader
		{
			get
			{
				return C.Client.RemoteEndPoint.ToString();
			}
		}
		protected TcpClient C;

		public bool Connected = false;

		void ClientCore()
		{
			DateTime T1 = DateTime.Now;
			while (Connected)
			{
				Thread.Sleep(1);
				if (C.Available != 0)
				{
					byte[] Msg = new byte[C.Available];
					C.Client.Receive(Msg, 4, SocketFlags.None);

					if (!ProcessMessage(Msg))
					{
					}
				}
				DateTime T2 = DateTime.Now;
				if (EchoUnderway)
				{
					EchoTimer = (T2 - T1).TotalSeconds;
					if (EchoTimer > 5)
						throw new Exception("Echo timeout (5sec)");
				}
				else
					EchoTimer = 0;
				T1 = T2;
			}
			if (C.Connected)
				C.Close();
			if (Disconnected != null)
				Disconnected(this);
			C = null;
		}

		public NWClient(TcpClient client)
		{
			C = client;
			Connected = true;
		}
		public NWClient(String host, int port)
		{
			Connect(host, port);
		}
		public void Connect(string host, int port)
		{
			C = new TcpClient();
			try
			{
				C.Connect(host, port);
				Connected = true;
				Start();
			}
			catch
			{
				Connected = false;
			}
		}
		public void Start()
		{
			Thread T = new Thread(ClientCore);
			T.Name = "Client core";
			T.Start();
		}

		public event NWClientEvent EchoReceived;
		public event NWClientEvent EchoReplied;

		public event NWClientDataEvent OnData;
		public event NWClientStringEvent OnString;

		bool EchoUnderway = false;
		double EchoTimer = 0;
		public void Echo()
		{
			EchoTimer = 0;
			try
			{
				SendCode(NWCode.ECHO1);
				EchoUnderway = true;
			}
			catch
			{
				EchoUnderway = false;
				EchoTimer = 0;
			}
		}

		//Virtual function to exit received on client side
		public event NWClientEvent ExitReceived;
		void ProcessExit()
		{
			if (ExitReceived != null)
				ExitReceived(this);
			ShutdownCore();
		}
		bool ProcessMessage(byte[] Msg)
		{
			if (Msg.Length < 4)
				throw new Exception("Invalid message");
			NWCode OPCODE = (NWCode)BitConverter.ToInt32(Msg, 0);
			switch (OPCODE)
			{
				case NWCode.EXIT:
					ProcessExit();
					return true;
				case NWCode.ECHO1:
					EchoReceived(this);
					SendCode(NWCode.ECHO2);
					return true;
				case NWCode.ECHO2:
					EchoUnderway = false;
					EchoTimer = 0;
					EchoReplied(this);
					return true;
				case NWCode.STRING:
				case NWCode.DATA:
					{
						if (Msg.Length < 4)
							throw new Exception("Invalid header");
						//byte[] MsgLen = new byte[4];
						//NetworkStream NS = new NetworkStream(C.Client);
						BinaryReader R = new BinaryReader(new NetworkStream(C.Client)/*NS*/);
						int DataLength = R.ReadInt32();
						byte[] Buf = new byte[DataLength];
						int Ptr = 0;
						while (Ptr < Buf.Length)
						{
							int NSeg = C.Client.Receive(Buf, Ptr, DataLength - Ptr, SocketFlags.None);
							if (NSeg == 0)
								Thread.Sleep(1);
							Ptr += NSeg;
							//Console.WriteLine("PTR: " + Ptr.ToString().PadLeft(10) + " " + NSeg.ToString().PadLeft(10));
						}
						//Ptr+= C.Client.Receive(Buf, Ptr,DataLength-Ptr, SocketFlags.None);
						if (OPCODE == NWCode.STRING)
						{
							string S = Encoding.Default.GetString(Buf);
							OnString(this, S);
							return true;
						}
						else
						{
							if (OnData != null)
								OnData(this, Buf);
							MsgBuf = Buf;
							MsgPtr = 0;
							return _ProcessMessage();
						}
					}
				default:
					throw new Exception("Invalid code received");
			}
		}
		protected int MsgPtr = 0;
		protected byte[] MsgBuf;

		public static byte[] MakePacket(int header,params object[] items)
		{
			List<byte> Res = new List<byte>();
			Res.AddRange(BitConverter.GetBytes(header));
			foreach (object o in items)
			{
				Type T = o.GetType();
				if (T == typeof(int))
					PackInt32(ref Res, (int)o);
				else if (T == typeof(long))
					PackInt64(ref Res, (long)o);
				else if (T == typeof(float))
					PackSingle(ref Res, (float)o);
				else if (T == typeof(double))
					PackDouble(ref Res, (double)o);
				else if (T == typeof(String))
					PackString(ref Res, (string)o);
				else if (T == typeof(byte[]))
				{
					PackInt32(ref Res, ((byte[])o).Length);
					PackByteArray(ref Res, (byte[])o);
				}
				else
					throw new Exception("Not supported object type, try to unfold to more primitive parts!");
			}
			return Res.ToArray();
		}
		public void SendPacket(int header, params object[] items)
		{
			SendData(MakePacket(header, items));
		}
		public void SendString(String S)
		{
			SendData(Encoding.Default.GetBytes(S), true);
		}

		#region Network Data Methods
		protected byte[] FetchArray(int N)
		{
			byte[] res = new byte[N];
			Buffer.BlockCopy(MsgBuf, MsgPtr, res, 0, N);
			MsgPtr += N;
			return res;
		}
		protected byte FetchByte()
		{
			MsgPtr ++;
			return MsgBuf[MsgPtr - 1];
		}
		public static int PackInt32(byte[] buf, int ptr, byte value)
		{
			Buffer.BlockCopy(new byte[] { value }, 0, buf, ptr, 1);
			return ptr + 1;
		}
		public static void PackInt32(ref List<byte> Res, byte value)
		{
			Res.Add(value);
		}
		protected int FetchInt32()
		{
			MsgPtr += 4;
			return BitConverter.ToInt32(MsgBuf, MsgPtr - 4);
		}
		public static int PackInt32(byte[] buf, int ptr, int value)
		{
			Buffer.BlockCopy(BitConverter.GetBytes(value), 0, buf, ptr, 4);
			return ptr + 4;
		}
		public static void PackInt32(ref List<byte> Res, int value)
		{
			Res.AddRange(BitConverter.GetBytes(value));
		}
		protected long FetchInt64()
		{
			MsgPtr += 8;
			return BitConverter.ToInt64(MsgBuf, MsgPtr - 8);
		}
		public static int PackInt64(byte[] buf, int ptr, long value)
		{
			Buffer.BlockCopy(BitConverter.GetBytes(value), 0, buf, ptr, 8);
			return ptr + 8;
		}
		public static void PackInt64(ref List<byte> Res, long value)
		{
			Res.AddRange(BitConverter.GetBytes(value));
		}

		protected float FetchSingle()
		{
			MsgPtr += 4;
			return BitConverter.ToSingle(MsgBuf, MsgPtr - 4);
		}
		public static int PackSingle(byte[] buf, int ptr, float value)
		{
			Buffer.BlockCopy(BitConverter.GetBytes(value), 0, buf, ptr, 4);
			return ptr + 4;
		}
		public static void PackSingle(ref List<byte> Res, float value)
		{
			Res.AddRange(BitConverter.GetBytes(value));
		}
		protected double FetchDouble()
		{
			MsgPtr += 8;
			return BitConverter.ToDouble(MsgBuf, MsgPtr - 8);
		}
		public static int PackDouble(byte[] buf, int ptr, double value)
		{
			Buffer.BlockCopy(BitConverter.GetBytes(value), 0, buf, ptr, 8);
			return ptr + 8;
		}
		public static void PackDouble(ref List<byte> Res, double value)
		{
			Res.AddRange(BitConverter.GetBytes(value));
		}

		protected string FetchStringS()
		{
			int L = FetchInt32();
			MsgPtr += L;
			return Encoding.Default.GetString(MsgBuf, MsgPtr - L, L);
		}
		public static int PackString(byte[] buf, int ptr, string s)
		{
			byte[] stringbuf = Encoding.Default.GetBytes(s);
			ptr = PackInt32(buf, ptr, stringbuf.Length);
			Buffer.BlockCopy(stringbuf, 0, buf, ptr, stringbuf.Length);
			return ptr + stringbuf.Length;
		}
		public static void PackString(ref List<byte> Res, string s)
		{
			byte[] stringbuf = Encoding.Default.GetBytes(s);
			PackInt32(ref Res, stringbuf.Length);
			Res.AddRange(stringbuf);
		}

		public static void PackByteArray(ref List<byte> Res, byte[] ba)
		{
			Res.AddRange(ba);
		}
		#endregion

		protected virtual bool _ProcessMessage()
		{
			return true;
		}
		public NWClient()
		{
		}
		public void Connect()
		{
		}

		//Close connection on client side
		public event NWClientEvent Disconnected;

		public void ShutdownCore()
		{
			Connected = false;
		}
		public void Disconnect()
		{
			if (Connected)
			{
				SendCode(NWCode.EXIT);
				ShutdownCore();
			}
			else
			{
				Console.WriteLine("Client already exited");
			}
		}

		public void SendData(INWObject obj)
		{
			SendData(obj.NWData);
		}
		public void SendData(byte[] buf, bool isstring = false)
		{
			if (Connected)
			{
				byte[] header = new byte[8];
				Buffer.BlockCopy(BitConverter.GetBytes((int)(isstring ? NWCode.STRING : NWCode.DATA)), 0, header, 0, 4);
				Buffer.BlockCopy(BitConverter.GetBytes(buf.Length), 0, header, 4, 4);
				C.Client.Send(header);
				C.Client.Send(buf);
			}
			else
			{
				Console.WriteLine("Client not connected, data was not sent!");
			}
		}
		public void SendCode(NWCode code)
		{
			C.Client.Send(BitConverter.GetBytes((int)code));
		}

		public static void DataHash(NWClient sender, byte[] data)
		{
			Console.WriteLine("Collected " + data.Length + " bytes");
			System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
			for (int i = 0; i < data.Length; i++)
			{
				Console.Write(data[i].ToString("X2"));
				if (i % 16 == 15)
					Console.WriteLine();
				else
					Console.Write("-");
			}
			String X = BitConverter.ToString(md5.ComputeHash(data));

		}
		public static void StringHandler(NWClient sender, string s)
		{
			Console.WriteLine("Client received the following string:\n" + s);
		}
	}
}